package fr.loof.logeek.maven;

/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache License, Version 2.0 (the
 * "License"); you may not use this file except in compliance
 * with the License.  You may obtain a copy of the License at
 *
 *   http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing,
 * software distributed under the License is distributed on an
 * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
 * KIND, either express or implied.  See the License for the
 * specific language governing permissions and limitations
 * under the License.
 */

import java.io.File;
import java.io.IOException;
import java.util.Collection;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtField;
import javassist.NotFoundException;

import org.apache.maven.artifact.Artifact;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.codehaus.plexus.logging.LogEnabled;
import org.codehaus.plexus.logging.Logger;
import org.codehaus.plexus.logging.console.ConsoleLogger;
import org.codehaus.plexus.util.DirectoryScanner;

import fr.loof.logeek.CodeTransformer;
import groovy.lang.GroovyClassLoader;

/**
 * Instruments bytecode
 *
 * @author <a href="mailto:nicolas@apache.org">Nicolas De Loof</a>
 * @goal instrument
 * @phase process-classes
 * @requiresDependencyResolution compile
 */
public class InstrumentMojo
    extends AbstractMojo
{
    /**
     * Location of the class files.
     *
     * @parameter expression="${project.build.outputDirectory}"
     * @required
     */
    private File outputDirectory;

    /**
     * @parameter expression="${project}"
     * @required
     * @readonly
     */
    private MavenProject project;

    /**
     * @parameter
     */
    private String[] includes;

    /**
     * @parameter
     */
    private String[] excludes;

    /**
     * @parameter
     * @required
     */
    private String transformer;

    /**
     * @parameter default-value="true"
     */
    private boolean failOnError;

    /**
     * @parameter default-value="false" expression="${instrumentation.skip}"
     */
    private boolean skip;

    private CodeTransformer ct;

    /**
     * CodeTransformer implemented as a Groovy Script. Can be used to subclass provided implementations for
     * customization
     *
     * @parameter
     */
    private String script;

    private ClassPool cp = ClassPool.getDefault();

    public void execute()
        throws MojoExecutionException
    {
        if ( skip )
        {
            return;
        }

        int level = Logger.LEVEL_WARN;
        if ( getLog().isInfoEnabled() )
        {
            level = Logger.LEVEL_INFO;
        }
        if ( getLog().isDebugEnabled() )
        {
            level = Logger.LEVEL_DEBUG;
        }
        Logger logger = new ConsoleLogger( level, "InstrumentationMojo" );

        try
        {
            Class ctClass;
            if (script != null)
            {
                getLog().info( "Instrument code using provided script" );
                GroovyClassLoader gcl = new GroovyClassLoader( getClass().getClassLoader() );
                ctClass = gcl.parseClass( script );
            }
            else
            {
                ctClass = Class.forName( transformer );
            }
            ct = (CodeTransformer) ctClass.newInstance();
            if ( ct instanceof LogEnabled )
            {

                ( (LogEnabled) ct ).enableLogging( logger );
            }
        }
        catch ( Exception e )
        {
            throw new MojoExecutionException( "Failed to instanciate CodeTransformer " + transformer, e );
        }

        setupClasspath();
        for ( String path : scan().getIncludedFiles() )
        {
            try
            {
                CtClass cl = cp.get( getClassName( path ) );

                if ( !isAllreadyInstrumented( cl ) )
                {
                    if ( ct.instrumentClass( cl, cp ) )
                    {
                        getLog().info( "Instrumented " + cl.getName() );
                        cl.writeFile( outputDirectory.getAbsolutePath() );
                    }
                }
            }
            catch ( NotFoundException e )
            {
                if ( !failOnError )
                {
                    getLog().warn( "NotFound failure instrumenting " + path );
                    continue;
                }
                throw new MojoExecutionException( "NotFound failure instrumenting " + path, e );
            }
            catch ( CannotCompileException e )
            {
                if ( !failOnError )
                {
                    getLog().warn( "Syntax error instrumenting " + path );
                    continue;
                }
                throw new MojoExecutionException( "Syntax error instrumenting " + path, e );
            }
            catch ( IOException e )
            {
                if ( !failOnError )
                {
                    getLog().warn( "Failed to write class back to file " + path );
                    continue;
                }
                throw new MojoExecutionException( "Failed to write class back to file " + path, e );
            }
            catch ( Exception e )
            {
                if ( !failOnError )
                {
                    getLog().warn( "Syntax error instrumenting " + path );
                    continue;
                }
                throw new MojoExecutionException( "Unexpected error instrumenting " + path, e );
            }
        }

    }

    private void setupClasspath()
        throws MojoExecutionException
    {
        try
        {
            cp.insertClassPath( outputDirectory.getAbsolutePath() );
            Collection<Artifact> artifacts = (Collection<Artifact>) project.getCompileArtifacts();
            getLog().debug( "append classpath artifacts " + artifacts.size() );
            for ( Artifact artifact : artifacts )
            {
                getLog().debug( "append classpath artifact " + artifact + " = " + artifact.getFile() );
                cp.appendClassPath( artifact.getFile().getAbsolutePath() );
            }
        }
        catch ( NotFoundException e )
        {
            throw new MojoExecutionException( "ClassPool setup failure", e );
        }
    }

    private String getClassName( String path )
    {
        return path.substring( 0, path.length() - ".class".length() ).replace( File.separatorChar, '.' );
    }

    private boolean isAllreadyInstrumented( CtClass cl )
        throws CannotCompileException
    {
        boolean allreadyInstrumented = false;
        try
        {
            cl.getField( "__INSTRUMENTED" );
            allreadyInstrumented = true;
        }
        catch ( NotFoundException e )
        {
            cl.addField( CtField.make( "private static final transient boolean __INSTRUMENTED = true;", cl ) );
        }
        return allreadyInstrumented;
    }

    private DirectoryScanner scan()
    {
        DirectoryScanner scanner = new DirectoryScanner();
        scanner.setBasedir( outputDirectory );
        if ( includes == null )
        {
            includes = new String[] { "**/*.class" };
        }
        scanner.setIncludes( includes );

        if ( excludes != null )
        {
            scanner.setExcludes( excludes );
        }
        scanner.scan();
        return scanner;
    }

}
